package cn.itrigger.dynamic.sql.where.render;

import cn.itrigger.dynamic.sql.condition.*;
import cn.itrigger.dynamic.sql.render.RenderingStrategy;
import cn.itrigger.dynamic.sql.render.TableAliasCalculator;
import cn.itrigger.dynamic.sql.select.render.SelectRenderer;
import cn.itrigger.dynamic.sql.select.render.SelectStatementProvider;
import cn.itrigger.dynamic.sql.structure.BindableColumn;
import cn.itrigger.dynamic.sql.util.FragmentAndParameters;
import cn.itrigger.dynamic.sql.util.FragmentCollector;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author admin
 * @date 2018/9/25
 **/
public class WhereConditionVisitor<T> implements ConditionVisitor<T, FragmentAndParameters> {

    private RenderingStrategy renderingStrategy;
    private AtomicInteger sequence;
    private BindableColumn column;
    private TableAliasCalculator tableAliasCalculator;
    private String parameterPrefix;

    private WhereConditionVisitor(Builder<T> builder) {
        this.renderingStrategy = Objects.requireNonNull(builder.renderingStrategy);
        this.sequence = Objects.requireNonNull(builder.sequence);
        this.column = Objects.requireNonNull(builder.column);
        this.tableAliasCalculator = Objects.requireNonNull(builder.tableAliasCalculator);
        this.parameterPrefix = Objects.requireNonNull(builder.parameterPrefix);
    }

    @Override
    public FragmentAndParameters visit(AbstractSingleValueCondition<T> condition) {
        String mapKey = formatParameterMapKey(sequence.getAndIncrement());
        String fragment = condition.renderCondition(columnName(), getFormattedJdbcPlaceholder(mapKey));

        return FragmentAndParameters.withFragment(fragment)
                .withParameter(mapKey, condition.value())
                .build();
    }

    @Override
    public FragmentAndParameters visit(AbstractListValueCondition<T> condition) {
        FragmentCollector fc = condition.mapValues(this::toFragmentAndParameters)
                .collect(FragmentCollector.collect());

        return FragmentAndParameters.withFragment(condition.renderCondition(columnName(), fc.fragments()))
                .withParameter(fc.parameters())
                .build();
    }

    @Override
    public FragmentAndParameters visit(AbstractNoValueCondition<T> condition) {
        return FragmentAndParameters.withFragment(condition.renderCondition(columnName()))
                .build();
    }

    @Override
    public FragmentAndParameters visit(AbstractTwoValueCondition<T> condition) {
        String mapKey1 = formatParameterMapKey(sequence.getAndIncrement());
        String mapKey2 = formatParameterMapKey(sequence.getAndIncrement());
        String fragment = condition.renderCondition(columnName(),
                getFormattedJdbcPlaceholder(mapKey1),getFormattedJdbcPlaceholder(mapKey2));

        return FragmentAndParameters.withFragment(fragment)
                .withParameter(mapKey1, condition.value1())
                .withParameter(mapKey2, condition.value2())
                .build();
    }

    @Override
    public FragmentAndParameters visit(AbstractSubselectCondition<T> condition) {
        SelectStatementProvider selectStatement = SelectRenderer.withSelectModel(condition.getSelectModel())
                .withRenderingStrategy(renderingStrategy)
                .withSequence(sequence)
                .build()
                .render();

        String fragment = condition.renderCondition(columnName(), selectStatement.getSelectStatement());

        return FragmentAndParameters.withFragment(fragment)
                .withParameter(selectStatement.getParameters())
                .build();
    }

    @Override
    public FragmentAndParameters visit(AbstractColumnComparisonCondition<T> condition) {
        String fragment = condition.renderCondition(columnName(), tableAliasCalculator);
        return FragmentAndParameters.withFragment(fragment).build();
    }

    private FragmentAndParameters toFragmentAndParameters(Object value) {
        String mapKey = formatParameterMapKey(sequence.getAndIncrement());

        return FragmentAndParameters.withFragment(getFormattedJdbcPlaceholder(mapKey))
                .withParameter(mapKey,value)
                .build();
    }

    private String formatParameterMapKey(int number) {
        return "p" + number;
    }

    private String getFormattedJdbcPlaceholder(String mapKey) {
        return renderingStrategy.getFormattedJdbcPlaceholder(column, parameterPrefix, mapKey);
    }

    private String columnName() {
        return column.renderWithTableAlias(tableAliasCalculator);
    }

    public static <T> Builder<T> withColumn(BindableColumn<T> column) {
        return new Builder<T>().withColumn(column);
    }

    public static class Builder<T> {
        private static final String DEFAULT_PARAMETER_PREFIX = "parameters";
        private RenderingStrategy renderingStrategy;
        private AtomicInteger sequence;
        private BindableColumn column;
        private TableAliasCalculator tableAliasCalculator;
        private String parameterPrefix = DEFAULT_PARAMETER_PREFIX;

        public Builder<T> withSequence(AtomicInteger sequence) {
            this.sequence = sequence;
            return this;
        }

        public Builder<T> withRenderingStrategy(RenderingStrategy renderingStrategy) {
            this.renderingStrategy = renderingStrategy;
            return this;
        }

        public Builder<T> withColumn(BindableColumn column) {
            this.column = column;
            return this;
        }

        public Builder<T> withTableAliasCalculator(TableAliasCalculator tableAliasCalculator) {
            this.tableAliasCalculator = tableAliasCalculator;
            return this;
        }

        public Builder<T> withParameterName(String parameterName) {
            parameterPrefix = Optional.ofNullable(parameterName)
                    .map(pn -> pn + "." + DEFAULT_PARAMETER_PREFIX)
                    .orElse(DEFAULT_PARAMETER_PREFIX);
            return this;
        }

        public WhereConditionVisitor<T> build() {
            return new WhereConditionVisitor<T>(this);
        }
    }
}
